﻿namespace Contoso.ActiveDirectory {

    #region Usings
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.DirectoryServices;
    using System.DirectoryServices.AccountManagement;
    using System.Linq;
    using System.Security.AccessControl;
    using System.Security.Principal;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading;
    using System.Threading.Tasks; 
    #endregion

    public class ComputerAccountMethods : UserComputerMethods {

        /// <summary>
        /// Gets the ComputerPrincipal object for the specified Domain and SamAccountName
        /// </summary>
        /// <param name="domain">The domain name</param>
        /// <param name="samAccountName">The computer SamAccountName</param>
        /// <returns>The ComputerPrincipal</returns>
        public ComputerPrincipal CreateComputerPrincipal(string domain, string samAccountName) {
            if (string.IsNullOrWhiteSpace(domain)) {
                throw new ArgumentNullException("domain");
            }
            if (string.IsNullOrWhiteSpace(domain)) {
                throw new ArgumentNullException("samAccountName");
            }

            var password = samAccountName;
            if (!password.EndsWith("$")) {
                password = password + "$";
            }
            return this.CreateComputerPrincipal(domain, samAccountName, password);
        }

        /// <summary>
        /// Gets the ComputerPrincipal object for the specified Domain and SamAccountName
        /// </summary>
        /// <param name="domain">The domain name</param>
        /// <param name="samAccountName">The computer SamAccountName</param>
        /// <param name="password">The password.</param>
        /// <returns>The ComputerPrincipal</returns>
        /// <remarks>When creating a new computer account or resetting an account, the password is traditionally set to samAccountName + $</remarks>
        public ComputerPrincipal CreateComputerPrincipal(string domain, string samAccountName, string password) {
            Debug.WriteLine(string.Format(@"{0}  [ThreadID: {1}]  {2} for domain\samAccountName: {3}\{4}", DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), string.IsNullOrWhiteSpace(domain) ? "NULL" : domain, string.IsNullOrWhiteSpace(samAccountName) ? "NULL" : samAccountName));

            #region Validation
            if (string.IsNullOrWhiteSpace(domain) || string.IsNullOrWhiteSpace(samAccountName)) {
                throw new ApplicationException(string.Format("Parameter(s) not specified.  domainNetBiosName: {0} samAccountName: {1}",
                    domain != null ? domain : "NULL", samAccountName != null ? samAccountName : "NULL"));
            }

            DomainConfiguration domainInfo = ADDataAccess.ADDomainInfo.Where(x => string.Equals(x.DistinguishedName, domain, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
            if (domainInfo == null) domainInfo = ADDataAccess.ADDomainInfo.Where(x => string.Equals(x.NetbiosName, domain, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
            if (domainInfo == null) {
                throw new ApplicationException(string.Format("Domain not found: {0}", domain));
            }

            if (!IsComputerSamAccountNameValid(samAccountName)) {
                throw new InvalidOperationException(string.Format("samAccountName invalid: {0}", samAccountName));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                DomainNetbiosName = domainInfo.NetbiosName,
                SamAccountName = samAccountName
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, ComputerPrincipal>(
                    ref methodParameters,
                    (x) => {
                        ComputerPrincipal computerPrincipal = null;
                        var availableDC = ConnectionManager.SelectDC(domainNetBiosName: methodParameters.DomainNetbiosName, forUpdate: true);
                        methodParameters.ServerNameOrAdjustedBindPath = availableDC.DNSName;

                        if (availableDC != null) {
                            PrincipalContext pc = new PrincipalContext(ContextType.Domain, availableDC.DNSName);
                            computerPrincipal = new ComputerPrincipal(pc, samAccountName, password, enabled: true);
                            computerPrincipal.Save();
                        }
                        else {
                            throw new ApplicationException(string.Format("Domain controller not found for domain: {0}", domain));
                        }

                        return computerPrincipal;
                    });
        }

        /// <summary>
        /// Disables the specified account
        /// </summary>
        /// <param name="dn">The distinguishedName of the account</param>
        /// <returns>True if successful</returns>
        public bool DisableAccount(string dn) {
            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ArgumentNullException("dn");
            }
            var account = ADDataAccess.GetADObjectProperties<ComputerAccount>(dn, null, string.Format("distinguishedName={0}", dn), PartitionScope.LocalPartition).SingleOrDefault();
            if (account == null) {
                throw new ApplicationException(string.Format("Unable to locate Computer account for DN: {0}", dn));
            }
            return this.DisableAccount(account);
        }

        /// <summary>
        /// Disables the specified account
        /// </summary>
        /// <param name="account">The UserProperties object for the account</param>
        /// <returns>True if successful</returns>
        public bool DisableAccount(UserAccount account) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} for DN: {3}", DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), ((account != null) && !string.IsNullOrWhiteSpace(account.DN)) ? account.DN : "NA"));

            return base.DisableAccount(account);
        }

        /// <summary>
        /// Enables the specified account
        /// </summary>
        /// <param name="dn">The distinguishedName of the account</param>
        /// <returns>True if successful</returns>
        public bool EnableAccount(string dn) {
            if (string.IsNullOrWhiteSpace(dn)) {
                throw new ArgumentNullException("dn");
            }
            var account = ADDataAccess.GetADObjectProperties<ComputerAccount>(dn, null, string.Format("distinguishedName={0}", dn), PartitionScope.LocalPartition).SingleOrDefault();
            if (account == null) {
                throw new ApplicationException(string.Format("Unable to locate Computer account for DN: {0}", dn));
            }
            return this.EnableAccount(account);
        }

        /// <summary>
        /// Enables the specified account
        /// </summary>
        /// <param name="account">The ComputerProperties object for the account</param>
        /// <returns>True if successful</returns>
        public bool EnableAccount(ComputerAccount account) {
            Debug.WriteLine(string.Format("{0}  [ThreadID: {1}]  {2} for DN: {3}", DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), ((account != null) && !string.IsNullOrWhiteSpace(account.DN)) ? account.DN : "NA"));

            return base.EnableAccount(account);
        }

        /// <summary>
        /// Gets the ComputerPrincipal object for the specified Domain and SamAccountName
        /// </summary>
        /// <param name="domain">The domain name</param>
        /// <param name="samAccountName">The computer SamAccountName.  May include $ appended to name or not.</param>
        /// <returns>The ComputerPrincipal</returns>
        public ComputerPrincipal GetComputerPrincipal(string domain, string samAccountName) {
            Debug.WriteLine(string.Format(@"{0}  [ThreadID: {1}]  {2} for domain\samAccountName: {3}\{4}", DateTime.Now.TimeOfDay, Thread.CurrentThread.ManagedThreadId, CustomExtensions.CurrentMethodName(), string.IsNullOrWhiteSpace(domain) ? "NULL" : domain, string.IsNullOrWhiteSpace(samAccountName) ? "NULL" : samAccountName));

            #region Validation
            if (string.IsNullOrWhiteSpace(domain) || string.IsNullOrWhiteSpace(samAccountName)) {
                throw new ApplicationException(string.Format("Parameter(s) not specified.  domainNetBiosName: {0} samAccountName: {1}",
                    domain != null ? domain : "NULL", samAccountName != null ? samAccountName : "NULL"));
            }

            DomainConfiguration domainInfo = ADDataAccess.ADDomainInfo.Where(x => string.Equals(x.DistinguishedName, domain, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
            if (domainInfo == null) domainInfo = ADDataAccess.ADDomainInfo.Where(x => string.Equals(x.NetbiosName, domain, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
            if (domainInfo == null) {
                throw new ApplicationException(string.Format("Domain not found: {0}", domain));
            }
            #endregion

            var methodParameters = new MethodParameters() {
                ServerNameOrAdjustedBindPath = null,
                DomainNetbiosName = domainInfo.NetbiosName,
                SamAccountName = samAccountName
            };

            return ConnectionManager.DirectoryServerHandler<MethodParameters, ComputerPrincipal>(
                    ref methodParameters,
                    (x) => {
                        ComputerPrincipal computerPrincipal = null;
                        var availableDC = ConnectionManager.SelectDC(methodParameters.DomainNetbiosName);
                        methodParameters.ServerNameOrAdjustedBindPath = availableDC.DNSName;

                        if (availableDC != null) {
                            PrincipalContext pc = new PrincipalContext(ContextType.Domain, availableDC.DNSName);
                            computerPrincipal = ComputerPrincipal.FindByIdentity(pc, methodParameters.SamAccountName);
                        }

                        return computerPrincipal;
                    });
        }

        /// <summary>
        /// Grantes the permissions necessary to join a computer to the domain
        /// </summary>
        /// <param name="computerDN">The computer distinguished name</param>
        /// <param name="securityPrincipalDN">The distinguished name of the security principal that will be granted the permission</param>
        /// <returns>True if successful</returns>
        public bool GrantDomainJoinPermission(string computerDN, string securityPrincipalDN) {
            if (string.IsNullOrWhiteSpace(computerDN)) {
                throw new ArgumentNullException("computerDN");
            }
            if (string.IsNullOrWhiteSpace(securityPrincipalDN)) {
                throw new ArgumentNullException("securityPrincipalDN");
            }
            string filter = string.Format("distinguishedName={0}", securityPrincipalDN);
            var securityPrincipals = ADDataAccess.GetADObjectProperties<UserGroupComputer>(ADDataAccess.ForestRootDN, null, filter, PartitionScope.GlobalCatalog);
            if (securityPrincipals.Count == 1) {
                return GrantDomainJoinPermission(computerDN, securityPrincipals.Single().ObjectSid);
            }
            else {
                return false;
            }
        }

        /// <summary>
        /// Grantes the permissions necessary to join a computer to the domain
        /// </summary>
        /// <param name="computerDN">The computer distinguished name</param>
        /// <param name="securityPrincipal">The security principal that will be granted the permission</param>
        /// <returns>True if successful</returns>
        public bool GrantDomainJoinPermission(string computerDN, IdentityReference securityPrincipal) {
            if (string.IsNullOrWhiteSpace(computerDN)) {
                throw new ArgumentNullException("computerDN");
            }
            if (securityPrincipal == null) {
                throw new ArgumentNullException("securityPrincipal");
            }

            if (!ADDataAccess.ObjectExists(computerDN)) return false;

            var activeDirectoryAccessRights = ActiveDirectoryRights.Delete | ActiveDirectoryRights.ReadControl | ActiveDirectoryRights.ListChildren | ActiveDirectoryRights.ReadProperty | ActiveDirectoryRights.DeleteTree | ActiveDirectoryRights.ExtendedRight;
            var accessRule = new ActiveDirectoryAccessRule(
                securityPrincipal, 
                activeDirectoryAccessRights,
                AccessControlType.Allow);

            if (!ADDataAccess.AddAccessRule(computerDN, accessRule)) return false;

            // Grant WriteProperty for ObjectType {4c164200-20c0-11d0-a768-00aa006e0529}	(Account Restrictions property set)
            accessRule = new ActiveDirectoryAccessRule(
                securityPrincipal,
                ActiveDirectoryRights.WriteProperty,
                AccessControlType.Allow,
                PropertySets.AccountRestrictions);
            if (!ADDataAccess.AddAccessRule(computerDN, accessRule)) return false;

            // Grant Self for ObjectType {f3a64788-5306-11d1-a9c5-0000f80367c1}	(Validated-SPN validated writes)
            accessRule = new ActiveDirectoryAccessRule(
                securityPrincipal,
                ActiveDirectoryRights.Self,
                AccessControlType.Allow,
                ExtendedRights.ValidatedSPN);
            if (!ADDataAccess.AddAccessRule(computerDN, accessRule)) return false;

            // Grant Self for ObjectType {72e39547-7b18-11d1-adef-00c04fd8d5cd}	(Validated-DNS-Host-Name validated writes)
            accessRule = new ActiveDirectoryAccessRule(
                securityPrincipal,
                ActiveDirectoryRights.Self,
                AccessControlType.Allow,
                PropertySets.DNSHostNameAttributes);
            if (!ADDataAccess.AddAccessRule(computerDN, accessRule)) return false;

            // Grant WriteProperty for ObjectType {3e0abfd0-126a-11d0-a060-00aa006c33ed}	(samAccountName)
            accessRule = new ActiveDirectoryAccessRule(
                securityPrincipal,
                ActiveDirectoryRights.WriteProperty,
                AccessControlType.Allow,
                new Guid("3e0abfd0-126a-11d0-a060-00aa006c33ed"));
            if (!ADDataAccess.AddAccessRule(computerDN, accessRule)) return false;

            // Grant WriteProperty for ObjectType {bf967953-0de6-11d0-a285-00aa003049e2}	(Display Name)
            accessRule = new ActiveDirectoryAccessRule(
                securityPrincipal,
                ActiveDirectoryRights.WriteProperty,
                AccessControlType.Allow,
                new Guid("bf967953-0de6-11d0-a285-00aa003049e2"));
            if (!ADDataAccess.AddAccessRule(computerDN, accessRule)) return false;

            // Grant WriteProperty for ObjectType {bf967950-0de6-11d0-a285-00aa003049e2}	(Description)
            accessRule = new ActiveDirectoryAccessRule(
                securityPrincipal,
                ActiveDirectoryRights.WriteProperty,
                AccessControlType.Allow,
                new Guid("bf967950-0de6-11d0-a285-00aa003049e2"));
            if (!ADDataAccess.AddAccessRule(computerDN, accessRule)) return false;

            // Grant WriteProperty for ObjectType {5f202010-79a5-11d0-9020-00c04fc2d4cf}	(Logon Information property set)
            accessRule = new ActiveDirectoryAccessRule(
                securityPrincipal,
                ActiveDirectoryRights.WriteProperty,
                AccessControlType.Allow,
                PropertySets.LogonInformation);
            if (!ADDataAccess.AddAccessRule(computerDN, accessRule)) return false;

            return true;
        }

        /// <summary>
        /// Invalid characters: \/[]:;|=,+*?<>"
        /// 15 character maximum
        /// Cannot start or end with space or end with . (dot, period)
        /// </summary>
        public bool IsComputerSamAccountNameValid(string samAccountName) {
            if (string.IsNullOrWhiteSpace(samAccountName)) return false;

            bool isValid = samAccountName.EndsWith("$") ? (samAccountName.Length <= 16) : (samAccountName.Length <= 15)
                && !samAccountName.EndsWith(".")
                && !samAccountName.StartsWith(" ")
                && !samAccountName.EndsWith(" ");

            if (isValid) {
                isValid = Regex.IsMatch(samAccountName, ADDataAccess.SamAccountNameDoesNotContainInvalidCharactersFilter);
            }

            return isValid;
        }

    }
}
